
require 'levenshtein'

module ActsAsSentence
  module Nameize
    def nameize
      self.to_s.gsub(/[^a-zA-Z0-9\?\=]/, ' ').gsub(/[ ]+/, ' ').strip
    end
  end
  
  class Sentence <  DelegateClass(String)
    def self.default_sentence_methods(obj)
	convert_array_to_sentence_methods obj.public_methods - Object.public_methods
    end

    def self.convert_array_to_sentence_methods(array)
      array.inject({}){|result, method| result[method.nameize] = method.to_sym if method; result}
    end
    
    def initialize(sentence, args=nil, &block)
      super(sentence.downcase)
      @lost_block = block
      @lost_args = a_sentence?(args) ? args.first : args
    end
    
    def to_readable
      readable = [self.to_s]
      readable << @lost_args.to_readable if @lost_args.respond_to?(:to_readable)
      readable.join(' ')
    end
    
    def find_similarest_in(array)
      similarest = array.collect{|element| {:distance => distance_with(element), :to_s => element}}.sort{|x, y| x[:distance] <=> y[:distance]}.first
      too_faraway?(similarest) ? nil : similarest[:to_s]
    end
    
    def call_similarest(obj, sentence_methods=nil)
      sentence_methods = sentence_methods || Sentence.default_sentence_methods(obj)
      
      if similarest = find_similarest_in(sentence_methods.keys)
        call obj, sentence_methods[similarest]
      else
        self
      end
    end
    
    private
    def orignal_args
      @lost_args.kind_of?(ActsAsSentence::Sentence) ? @lost_args.send(:orignal_args) : @lost_args
    end
    
    def orignal_block
      @lost_block || if @lost_args.kind_of?(ActsAsSentence::Sentence) then 
        @lost_args.send(:orignal_block)
      end
    end

    def a_sentence?(array)
      array && array.size == 1 && array.first.respond_to?(:to_readable)
    end
    
    def distance_with(str)
      str = str.downcase
      return max_effective_distance_with(str) unless start_with_first_word?(str)
      Text::Levenshtein.distance(str, self.to_readable)
    end
    
    def start_with_first_word?(str)
      str.index(first_word) == 0 || self.index(first_word(str)) == 0
    end
    
    def first_word(str=nil)
      (str || self).nameize.split(' ').first || ''
    end
    
    def max_effective_distance_with(str)
      Math.hypot(self.to_readable.size, str.size)/3
    end
    
    def too_faraway?(str)
      str[:distance] >= max_effective_distance_with(str[:to_s])
    end
    
    def call(obj, similarest_method)
      raise "Matched similar method(#{similarest_method}) by sentence(#{to_readable}), but not respond to it!" unless obj.respond_to? similarest_method

      obj.__send__ similarest_method, *(orignal_args || []), &orignal_block
    end
  end
  
  module Base
    def object_exists(name)
      # should be overwrited
    end
    
    def sentence_methods
      # should be overwrited
    end

    def __keywords_for_args_to_pass_through__
      [:a, :an, :be, :been, :am, :is, :was, :are, :were]
    end

    def method_missing(method, *args, &block)
      return object_exists(method) if args.empty? && object_exists(method)
      return *args if __keywords_for_args_to_pass_through__.include? method
      Sentence.new(method.nameize, args, &block).send(:call_similarest, self, sentence_methods)
    end
  end
  
end

class Symbol
  include ActsAsSentence::Nameize
end

class String
  include ActsAsSentence::Nameize
end
